##
## Physical.py -- central clearing house for measurements and the imprecision we are cursed with
##
""" This module implements physical measurements with estimated accuracies.
    Four sections
        1) tstamp
        2) Units of measurement and the functions that convert them
        3) Accuracy and AccuracyUnit -- specifies the range of values that a
           Measurement can have. Specified as a -val,+val from nominal.
        4) class Measurement -- applies accuracy to a nominal value.
"""
from collections import namedtuple
from functools import partial



## ###########################################################################
## ###########################################################################
## ###########################################################################

## timestamp

# bummer, python 3.2 doesn't have timestamp()
# and we can't yet get python 3.3 for raspberry
# so we kludge such a thing here

import datetime
import time
def tstamp(fr=None) :
    """ returns a float timestamp -- like python 3.3 and up
        if fr is given, it must be a datetime.datetime
        otherwise, tstamp returns the current time        
    """
    if fr is None :
        fr = datetime.datetime.now()
    return time.mktime( fr.timetuple() ) + fr.microsecond * 1.0e-6



## ###########################################################################
## ###########################################################################
## ###########################################################################

##  physical units
##
##  internal to the app we work in SI units
##  for data input and display we like more colloquial units
##  converter is a table of functions to convert
##  use the Physical.convert() function to do it for you


converter = {
# MASS
    'kg' :  {   'SI' : lambda x : converter['kg']['kg'](x) ,
                'kg' : lambda x : x  ,
                'gm' : lambda x : x * 1000.0,


            } ,
# DISTANCE
    'm'     : { 'SI' : lambda x : converter['m']['m'](x) ,
                'm'  : lambda x : x ,
                'in' : lambda x : x * 3.937007874015748E+1 ,
                'ft' : lambda x : x * 3.280839895013123
              } ,
    'ft'    : { 'SI' : lambda x : converter['ft']['m'](x) ,
                'ft' : lambda x : x ,
                'm'  : lambda x : x * 3.048E-1
              } ,
    'in' : {    'SI' : lambda x : converter['in']['m'](x) ,
                'in' : lambda x : x ,
                'ft' : lambda x : x * 8.333333333333333E-2 ,
                'm'  : lambda x : x * 2.54E-2
            } ,
    'ft,in' : { 'SI' : lambda x,y : converter['ft,in']['m'](x,y) ,
                'm' : lambda x,y : converter['ft']['m'](x) + converter['in']['m'](y)
              } ,
# TIME
    'sec'     : { 'SI' : lambda x : converter['sec']['sec'](x) ,
                  'sec' : lambda x : x  ,
                  'tick,rate' : lambda x,y : int(x*y) & 0xFFFFFFFF
                } ,
    'tick,rate' : { 'SI' : lambda x,y : converter['tick,rate']['sec'](x,y) ,
                    'sec' : lambda x,y : x/y
                } ,
# VELOCITY
    'mps' :{  'SI' : lambda x : converter['mps']['mps'](x) ,
              'mps' : lambda x : x ,
              'mph' : lambda x : x * 2.236936292054403,
              'fps' : lambda x : x * 3.280839895013123
           } ,
# ACCELERATION

# FORCE

    }


def convert( x, fr, to='SI' ) :

    if isinstance(x,tuple) :
        return converter[fr][to]( *x )
    return converter[fr][to]( x )





## ###########################################################################
## ###########################################################################
## ###########################################################################

## Accuracy


Accuracy = namedtuple('Accuracy', 'neg, pos')
    # goal: for some arbitrary physical value x, implement the concept of x-err and x+err
    # implementation: we provide namedtuple Accuracy with neg and pos fields
    #    neg MUST BE NEGATIVE or ZERO
    #    pos MUST BE POSITIVE or ZERO

AccuracyUnit = namedtuple( 'AccuracyUnit', [
    'acc'   ,   #Accuracy
    'unit'  ]   # string -- units to use
    )
    # attaches a physical unit to an Accuracy

def Accuracy_from_AccuracyUnit( au, additional=None ) :

    neg_x = au.acc.neg
    pos_x = au.acc.pos
    if additional is not None :
        # need to turn the args into tuples
        neg_x = ( neg_x , additional )
        pos_x = ( pos_x , additional )
    neg = convert( neg_x,au.unit,'SI' )
    pos = convert( pos_x,au.unit,'SI' )
    return Accuracy(neg,pos)


## ###########################################################################
## ###########################################################################
## ###########################################################################

## Measurement

class Measurement :
    """ The Physical.Measurement class blends a nominal physical measurement with a given Accuracy.
        Once constructed, instances behave like scalar variables -- sort of.
        Operations supported are initialization, iteration, printing, addtion, subtraction, and formula application.
    """
    def __init__(self,x,accuracy) :
        assert( accuracy.neg <= 0.0 )
        assert( accuracy.pos >= 0.0 )
        self.lo = x+accuracy.neg
        self.nom = x
        self.hi = x+accuracy.pos
    def __iter__(self) :
        yield self.lo
        yield self.nom
        yield self.hi
    def __add__(self,other) :
        ret = Measurement(self.nom, Accuracy(0.0,0.0))
        ret.lo  = self.lo  + other.lo   # lowest possible result
        ret.nom = self.nom + other.nom
        ret.hi  = self.hi  + other.hi   # highest possible result
        return ret
    def __sub__(self,other) :
        ret = Measurement(self.nom, Accuracy(0.0,0.0))
        ret.lo  = self.lo  - other.hi # lowest possible result -- assumes self > other ...
        ret.nom = self.nom - other.nom
        ret.hi  = self.hi  - other.lo # highest possible result -- assumes self > other ...
        return ret
    def __repr__(self) :
        return "Measurement(lo={},nom={},hi={})".format(self.lo, self.nom, self.hi)
    def __str__(self) : # i guess with no formating
        return "lo={:0.3f} nom={:0.3f} hi={:0.3f}".format(
            self.lo, self.nom,self.hi)
    def __format__(self,fmt) :
        """ applies fmt to each of .lo .nom .hi
            ex: m = some Measurement
                print( "nice looking {:3.1}".format(m) )
            note: it is useful to use # and g to get a set number of digits
              ex: {:#.4g} would print 30.10
        """
        f = "lo={:"+fmt+"} nom={:"+fmt+"} hi={:"+fmt+"}"
        return f.format(self.lo, self.nom,self.hi)

    def convert(self,fr,to) :
        ret = Measurement( 0,Accuracy(0,0) )
        ret.lo  = convert(self.lo,fr,to)
        ret.nom = convert(self.nom,fr,to)
        ret.hi  = convert(self.hi,fr,to)
        return ret



## now we get a function that can excersize a formula

def Measurement_from_function( func, arglist ) :
    """ given a function of the form
            float = func( arg0, arg1, arg2 ... )
        and an ordered iterable of Measurement args
        apply all possible measurement values in each arg
        and return a Measurement for the result
    """

    # determines the nominal value of ret
    def nominal_curry(func,argset) :
        if len(argset) :
            return nominal_curry(
                partial( func, argset[0].nom ), # strip out the arg
                argset[1:] ) # and send the rest to recursive
        # otherwise we got to the end
        return func()

    # get the nominal values -- assume infinite accuracry
    ret = Measurement(nominal_curry(func,arglist),Accuracy(0,0))

    # apply the curried form for all possible
    def variational_curry( func, argset ) :
        if len(argset) : # recursive case
            for arg in argset[0] : # for each of possible value in 'this' slot
                variational_curry(    # recurse with that value bound to func
                    partial( func, arg ), argset[1:] ) # and the remainder of the arg list
        else: # otherwise we are the base case
            x = func() # get the value
            if    x < ret.lo : ret.lo = x # new lo ?
            elif  x > ret.hi : ret.hi = x # new hi ?

    # apply it
    variational_curry( func, arglist )

    # and we should be done
    return ret


## ###################################################################################


# test code


#   def testme() :
#
#       # define the function
#
#       def velocity_3( dt10, dt21, dx10, dx21 ) :
#           return ( dx10*dt21*dt21 + dx21*dt10*dt10 ) / ( dt10*dt21*(dt10+dt21) )
#
#       # define some measures
#
#       time_accuracy = Accuracy(neg=  -0.1 , pos=  +0.0)
#       dist_accuracy = Accuracy(neg= -0.01 , pos= +0.01)
#
#       t0 = Measurement( 1, time_accuracy )
#       t1 = Measurement( 2, time_accuracy )
#       t2 = Measurement( 3, time_accuracy )
#
#       dt10 = t1 - t0
#       dt21 = t2 - t1
#
#       x0 = Measurement( 10, dist_accuracy )
#       x1 = Measurement( 20, dist_accuracy )
#       x2 = Measurement( 30, dist_accuracy )
#
#       dx10 = x1 - x0
#       dx21 = x2 - x1
#
#       #print()
#       #print( x0 )
#       #print( x1 )
#       #print( dx10 )
#       #print()
#
#       readings = [ dt10, dt21, dx10, dx21 ]
#
#       ret = Measurement_from_function(velocity_3, readings)
#
#       print(ret)
#

